//导入整个 three.js核心库
import * as THREE from 'three'

// 常用方法
import { MathUtils } from 'three/src/math/MathUtils'

// 控制器
import { OrbitControls } from './OrbitControls'
import { MapControls } from './MapControls'
import { raycaster, collideCheck, FirstPersonControls } from './FirstPersonControls'

// 加载器
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader'
import { OBJLoader } from 'three/addons/loaders/OBJLoader'
import { FBXLoader } from 'three/addons/loaders/FBXLoader'
// indexedDB数据库缓存模型，优化加载速度
import { DBUtil } from '@u/indexedDB'

/********************************************模型高亮显示/选中模型发光 ****************************************************/
// 效果合成器
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer'
// 渲染器通道
import { RenderPass } from 'three/addons/postprocessing/RenderPass'
import { OutlinePass } from 'three/addons/postprocessing/OutlinePass'
// 着色器通道
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass'
import { FXAAShader } from 'three/addons/shaders/FXAAShader'

// label标签
import { CSS2DObject, CSS2DRenderer } from 'three/addons/renderers/CSS2DRenderer'
import { CSS3DObject, CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer'

// TWEEN 动画
import { cameraControlsTwwen, cameraTwwen, TWEEN } from './tween'

// 经纬度坐标转换
import * as d3 from 'd3'

// 自定义ShaderMaterial
export * from './ShaderMaterial'

// glb加载器
const gltfLoader = new GLTFLoader().setDRACOLoader(new DRACOLoader().setDecoderPath('/three/gltf/'))

// 贴图加载器
const textureLoader = new THREE.TextureLoader()

/**
 * 打印相机位置和朝向  相机朝向也可以使用 controls.target 获取
 * @param {*} camera 相机
 * @param {*} controls 控制器
 */
function cameraDistance(camera, controls) {
	// 相机朝向
	const distance = camera.position.length()
	const { x, y, z } = camera.position
	const direction = camera.getWorldDirection(new THREE.Vector3())
	const target = new THREE.Vector3(x, y, z).add(direction.multiplyScalar(distance))
	console.log('controls.target', controls.target)
	console.log('target', target)
	console.log('position', camera.position)
}

/**
 * 模型居中显示
 * @param {*} scene 模型
 */
function setContent(scene) {
	// 获得包围盒得min和max
	const box = new THREE.Box3().setFromObject(scene)
	// 返回包围盒的中心点
	const center = box.getCenter(new THREE.Vector3())
	scene.position.x += scene.position.x - center.x
	scene.position.y += scene.position.y - center.y
	scene.position.z += scene.position.z - center.z
}

/**
 * 加载网格底图
 * @param {*} scene 场景  GridHelper属性
 */
function initGrid({ scene, size = 3000, step = 300, transparent = true, opacity = 0.5 } = {}) {
	const gridHelper = new THREE.GridHelper(size, step)
	gridHelper.material.transparent = transparent // 是否透明
	gridHelper.material.opacity = opacity // 透明度
	scene.add(gridHelper)
}

/**
 * threejs封装
 */
class renderModel {
	/**
	 * 类初始化函数
	 * @param {*} selector container选择器；例如：'#three'、'.three'等
	 * @param {*} center 场景的中心点经纬度
	 * @param {*} mapRadis 地球半径（以米为单位）默认：6371000
	 */
	constructor(selector, center, mapRadis = 6371000) {
		this.container = document.querySelector(selector)
		// 地球半径
		this.mapRadis = mapRadis
		// 相机
		this.camera
		// 场景
		this.scene
		//渲染器
		this.renderer
		//2d标签渲染器
		let label2dRenderer, lastLabel2dData
		this.label2dData = {}
		this.CSS2DObject = CSS2DObject
		//3d标签渲染器
		let label3dRenderer
		this.label3dData = {}
		this.CSS3DObject = CSS3DObject
		this.CSS3DSprite = CSS3DSprite
		// 控制器
		this.controls
		// 模型
		this.model
		this.progress = ref(0)
		this.loading = ref(false)
		// 加载进度监听
		this.loadingManager = new THREE.LoadingManager()
		//文件加载器类型
		const fileLoaderMap = {
			glb: gltfLoader,
			fbx: new FBXLoader(this.loadingManager),
			gltf: gltfLoader,
			obj: new OBJLoader(this.loadingManager)
		}
		// 贴图加载器
		this.textureLoader = textureLoader
		//模型动画列表
		this.modelAnimation
		//模型动画对象
		this.animationMixer
		this.animationClock = new THREE.Clock()
		this.animateClipAction
		// 动画循环方式枚举
		this.loopMap = {
			LoopOnce: THREE.LoopOnce,
			LoopRepeat: THREE.LoopRepeat,
			LoopPingPong: THREE.LoopPingPong
		}
		// 效果合成器
		let effectComposer, outlinePass
		this.outlinePassSelectedObjects = []
		// 动画渲染器
		let renderAnimation = null
		// 碰撞检测
		this.raycaster = new THREE.Raycaster()
		// 鼠标位置
		const mouse = new THREE.Vector2()
		this.mousePoint
		this.mouseLatlog
		// 场景渲染时需要执行的逻辑
		this.sceneAnimationEvents = {}
		// 动画部分
		this.cameraControlsTwwen = cameraControlsTwwen
		this.cameraTwwen = cameraTwwen
		this.TWEEN = TWEEN

		// D3.js 墨卡托投影
		this.projection = d3
			.geoMercator()
			.center(center)
			.scale(this.mapRadis) // 地球半径（以米为单位）6378137
			.translate([0, 0]) // 不进行平移

		const scope = this
		/**
		 * 初始化函数
		 * @returns Promise
		 */
		this.init = function ({ controls = 'OrbitControls', target, background = 'light', filePath, fileType, scale, position, autoPlay, center } = {}) {
			return new Promise(async (reslove, reject) => {
				this.loading.value = true
				//初始化渲染器
				initRender()
				//初始化相机
				initCamera()
				//初始化场景
				initScene(background)
				//初始化灯光
				initLight()
				//初始化渲染2d标签渲染器
				initLabel2dRenderer()
				//初始化渲染3d标签渲染器
				initLabel3dRenderer()
				//初始化控制器，控制摄像头,控制器一定要在渲染器后
				if (controls) initControls(controls, target)
				//监听窗口变化
				addEvenListMouseLisatener()
				//场景渲染
				sceneAnimation()
				// 创建效果合成器
				createEffectComposer()
				// 添加物体模型 TODO：初始化时需要默认一个
				if (filePath) {
					const load = await this.setModel({ filePath, fileType, scale, position, autoPlay, center })
					nextTick(() => {
						this.loading.value = false
					})
					reslove(load)
				}
			})
		}
		/**
		 * 销毁函数
		 */
		this.dispose = function () {
			window.removeEventListener('resize', onWindowResize)
			cancelAnimationFrame(renderAnimation)
			// 释放显存
			this.renderer?.dispose()
			this.controls?.dispose()
			// 释放内存
			this.renderAnimation = null
			this.container = null
			this.scene = null
			label2dRenderer = null
			label3dRenderer = null
		}
		/**
		 * 创建渲染器
		 */
		function initRender() {
			//设置抗锯齿
			this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true })
			//设置屏幕像素比
			this.renderer.setPixelRatio(window.devicePixelRatio)
			//渲染的尺寸大小
			const { clientHeight, clientWidth } = this.container
			this.renderer.setSize(clientWidth, clientHeight)
			//色调映射
			this.renderer.toneMapping = THREE.ReinhardToneMapping
			this.renderer.autoClear = true
			this.renderer.outputColorSpace = THREE.SRGBColorSpace
			//曝光
			this.renderer.toneMappingExposure = 3
			this.renderer.shadowMap.enabled = true
			this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
			this.container.appendChild(this.renderer.domElement)
		}
		/**
		 * 创建相机
		 */
		function initCamera() {
			const { clientHeight, clientWidth } = this.container
			this.camera = new THREE.PerspectiveCamera(50, clientWidth / clientHeight, 0.25, 2000)
		}
		/**
		 * 创建场景
		 */
		function initScene() {
			this.scene = new THREE.Scene()
			let texture
			if (background === 'light') {
				texture = this.textureLoader.load(`/three/CubeTexture/light.png`)
			} else {
				// - CubeTexture 是将六张图片拼成一个立方体，适合更加复杂的全景场景。
				texture = new THREE.CubeTextureLoader()
					.setPath('/three/CubeTexture/1/')
					.load(['Right.jpg', 'Left.jpg', 'Up.jpg', 'Down.jpg', 'Front.jpg', 'Back.jpg'])
			}
			this.scene.background = texture
			this.scene.environment = texture
		}
		/**
		 * 创建灯光
		 */
		function initLight() {
			this.scene.add(new THREE.AmbientLight('#ffffff', 1))
			let light = new THREE.PointLight('#ffffff', 0.5)
			light.position.set(100, 100, -100)
			this.scene.add(light)
		}
		/**
		 * 创建CSS2DRenderer渲染器
		 */
		function initLabel2dRenderer() {
			label2dRenderer = new CSS2DRenderer()
			label2dRenderer.domElement.style.position = 'absolute'
			label2dRenderer.domElement.style.top = 0
			label2dRenderer.domElement.style.pointerEvents = 'none'
			this.container.appendChild(label2dRenderer.domElement)
		}
		/**
		 * 创建CSS3DRenderer渲染器
		 */
		function initLabel3dRenderer() {
			label3dRenderer = new CSS3DRenderer()
			label3dRenderer.domElement.style.position = 'absolute'
			label3dRenderer.domElement.style.top = 0
			label3dRenderer.domElement.style.pointerEvents = 'none'
			this.container.appendChild(label3dRenderer.domElement)
		}
		/**
		 * 创建控制器
		 */
		function initControls(type, target) {
			const controls = { OrbitControls, MapControls, FirstPersonControls }[type]
			if (type !== 'FirstPersonControls') {
				this.controls = new controls(this.camera, this.renderer.domElement)
				target && this.controls.target.set(...target)
				this.controls.minPolarAngle = 0
				this.controls.maxPolarAngle = Math.PI / 2

				this.controls.addEventListener('change', () => {
					if (this.controls.eventType) this.container.style.cursor = `url('/${this.controls.eventType}.png'), default !important`
					// 添加需要聚合的标签
					this.label2dUpdate()
				})
				this.controls.addEventListener('end', () => {
					this.controls.eventType = null
					setTimeout(() => {
						this.container.style.cursor = `default`
					}, 500)
				})
			} else {
				this.controls = new controls(this.camera, this.renderer.domElement)
			}
		}
		/**
		 * 渲染场景
		 */
		function sceneAnimation() {
			renderAnimation = requestAnimationFrame(() => this.sceneAnimation())
			this.controls?.update?.()
			TWEEN.update()
			effectComposer?.render?.()
			this.animationMixer?.update?.(this.animationClock.getDelta())
			Object.values(this.sceneAnimationEvents)?.forEach((item) => {
				item?.()
			})
			if (this.controls.isFirstPersonControls) compassRotate()
			label2dRenderer.render(this.scene, this.camera)
			label3dRenderer.render(this.scene, this.camera)
			if (!isEqual(lastLabel2dData, this.label2dData)) label2dUpdate()
			this.renderer.render(this.scene, this.camera)
		}
		/**
		 * 2dlabel创建，更新逻辑
		 */
		this.label2dUpdate = debounce(() => {
			lastLabel2dData = this.label2dData
			// 添加需要聚合的标签
			Object.values(this.label2dData).forEach((item) => {
				updateClusters(item)
			})
		}, 200)
		/**
		 * 2dlabel创建，并进行聚合
		 * @param {Array} list 包含经纬度的数组[{id: 1,loglat: [90.411271669168, 44.53797016376461],name:'标注名称',alarm: true}]alarm是否有告警
		 */
		function updateClusters({ list, fillType: { id = id, alarm = alarm, name = name }, img = '/three/label/人员.png' } = {}) {
			// 过滤掉不在视野中的点
			const points = list.filter((point) => {
				point.position = this.LatlogToXyz(point.loglat)
				// 更新坐标
				pointLabel.position.set(...point.position)
				let pointLabel = scene.getObjectByName(point.id),
					visible = this.isEyes(point.position)
				if (pointLabel) pointLabel.visible = visible
				return visible
			})

			points.forEach((point, index) => {
				const cluster = [point]
				if (point.visible !== false) {
					// 计算该点之后相交的点组
					for (let i = index + 1, len = points.length; i < len; i++) {
						const nextPoint = points[i]
						if (this.shouldClusterPoints(point.position, nextPoint.position)) {
							nextPoint.visible = false
							cluster.push(nextPoint)
						}
					}
					point.cluster = cluster

					const pointLabel = scene.getObjectByName(point[id])
					if (pointLabel) {
						pointLabel.visible = point.visible
						if (pointLabel.visible) {
							const dom = document.getElementById(`${point[id]}_len`)
							if (dom.innerHTML == point.cluster.length) return
							dom.innerHTML = point.cluster.length == 1 ? point[name] : `${point.cluster.length}`
						}
					} else {
						const dom = document.createElement('div')
						dom.className = 'threeLabel'
						dom.id = point.id
						dom.innerHTML = `<img src="${img}" style="width:20px"/><span  class="len" id='${point.id}_len'>${
							point.cluster.length == 1 ? point[name] : point.cluster.length
						}</span>`
						dom.onclick = () => {
							labelTableDom(point)
						}
						const pointLabel = new CSS2DObject(dom)
						pointLabel.position.set(...point.position)
						pointLabel.center = new THREE.Vector2(0.5, 1)
						pointLabel.name = point.id
						scene.add(pointLabel)
					}
				}
			})
		}
		/**
		 * 创建labelTableDom元素 聚合元素点击事件逻辑
		 * @param {Object} point 标注的单条数据
		 */
		function labelTableDom(point) {
			// const dom = document.createElement('div')
			// dom.className = 'label'
			// dom.id = point.id
			// dom.innerHTML = `<img src="/人员.png" style="width:20px"/><span  class="len" id='${point.id}_len'>${
			// 	point.cluster.length == 1 ? point[name] : point.cluster.length
			// }</span>`
			// dom.onclick = () => {
			// 	console.log(point)
			// }
			// const pointLabel = new CSS2DObject(dom)
			// pointLabel.position.set(...point.position)
			// pointLabel.center = new THREE.Vector2(0.5, 1)
			// pointLabel.name = point.id
			// scene.add(pointLabel)
		}
		/**
		 * 判断是否在视角内
		 * @param {*} vector3 3维点
		 * @returns
		 */
		this.isEyes = function (vector3) {
			const tempV = new THREE.Vector3(...vector3).applyMatrix4(this.camera.matrixWorldInverse).applyMatrix4(this.camera.projectionMatrix)
			// 在视野外了
			if (Math.abs(tempV.x) > 1 || Math.abs(tempV.y) > 1 || Math.abs(tempV.z) > 1) return false
			// 在视野内
			return true
		}
		/**
		 * 检测传入的两个标注点是否需要聚合
		 * @param {*} point1 3维点
		 * @param {*} point2 3维点
		 * @param {*} threshold 聚合的阈值，20
		 * @returns
		 */
		this.shouldClusterPoints = function (point1, point2, threshold = 20) {
			const screenPos1 = this.getScreenPosition(point1),
				screenPos2 = this.getScreenPosition(point2),
				distance = Math.hypot(screenPos2.x - screenPos1.x, screenPos2.y - screenPos1.y)

			return distance < threshold
		}
		/**
		 * 3维坐标转屏幕坐标
		 * @param {*} vector3
		 * @returns
		 */
		this.getScreenPosition = function (vector3) {
			const vector = new THREE.Vector3(...vector3).project(this.camera)
			const halfWidth = this.container.clientWidth / 2
			const halfHeight = this.container.clientHeight / 2

			return {
				x: vector.x * halfWidth + halfWidth,
				y: -(vector.y * halfHeight) + halfHeight
			}
		}
		/**
		 * 经纬度转xyz
		 * @param {*} point 经纬度
		 * @returns
		 */
		this.LatlogToXyz = function (point) {
			const [x, z] = this.projection(point),
				y = this.getYCoordinate(x, z)
			return [x, y, z]
		}
		/**
		 * xyz转经纬度
		 * @param {*} point xyz坐标
		 * @returns
		 */
		this.XyzToLatlog = function (point) {
			return [...this.projection.invert([point.x, point.z]), point.y]
		}
		/**
		 * 获取xz坐标对应的y坐标
		 * @param {*} x x坐标
		 * @param {*} z z坐标
		 * @returns y坐标
		 */
		this.getYCoordinate = function (x, z) {
			// 设置射线投射的起点和方向
			const origin = new THREE.Vector3(x, this.mapRadis, z) // 假设从y=6371000开始向下投射
			const direction = new THREE.Vector3(0, -1, 0) // 方向是向下的y轴
			this.raycaster.set(origin, direction)
			// 计算交点
			const intersects = this.raycaster.intersectObjects(this.scene?.children || [])
			if (intersects.length > 0) {
				// 返回第一个交点的y坐标
				return intersects[0].point.y
			} else {
				// 如果没有交点，返回null
				return 0
			}
		}
		/**
		 * 加载模型
		 * @param {string} filePath 模型路径
		 * @param {string} fileType 模型类型
		 * @param {number,Array} scale 模型的放大倍数
		 * @param {Object,Array} position 模型的位置
		 * @param {Blob} autoPlay 模型动画自动播放
		 * @param {Blob} center 是否自动居中
		 * @returns Promise
		 */
		this.setModel = function ({ filePath, fileType = 'glb', scale, position, autoPlay = true, center = false } = {}) {
			return new Promise((resolve, reject) => {
				const loader = fileLoaderMap[fileType]
				// 使用indexedDB数据缓存请求的模型数据
				new DBUtil().get(filePath).then((blob) => {
					let url = URL.createObjectURL(blob)
					loader.load(
						url,
						(result) => {
							switch (fileType) {
								case 'gltf':
								case 'glb':
									this.model = result.scene
									break
								case 'fbx':
								case 'obj':
									this.model = result
									break
								default:
									break
							}
							this.animationMixer = new THREE.AnimationMixer(this.model)
							this.modelAnimation = result.animations || []
							this.animateClipAction = []
							this.modelAnimation.forEach((item) => {
								const action = this.animationMixer.clipAction(item)
								this.animateClipAction.push(action)
								autoPlay && action.play()
							})
							//	设置模型大小
							if (scale) {
								if (isArray(position)) {
									this.model.scale.set(...scale)
								} else {
									this.model.scale.set(scale, scale, scale)
								}
							}
							// 模型居中
							if (center) setContent(this.model)
							//设置模型位置
							if (position) {
								if (isArray(position)) {
									this.model.position.set(...position)
								} else {
									const { x, y, z } = position
									this.model.position.set(x, y, z)
								}
							}
							this.scene.add(this.model)
							resolve(true)
						},
						({ lengthComputable, loaded, total }) => {
							if (lengthComputable) {
								this.progress.value = Math.round((loaded / total) * 100, 2)
							} else {
								console.log('已加载' + loaded + '字节')
							}
						},
						(err) => {
							console.log(err)
							reject()
						}
					)
				})
			})
		}
		/**
		 * 模型动画启停
		 */
		this.animateClipActionChange = function () {
			this.animateClipAction.forEach((item) => {
				item.paused = !item.paused
				item.paused && item.play()
			})
		}
		/**
		 * 根据当前相机位置旋转罗盘
		 */
		function compassRotate() {
			const compass = document.getElementById('compass-pointer')
			if (compass) {
				// 获取相机方向向量 世界坐标 返回一个表示该物体在世界空间中Z轴正方向的矢量。
				const dir = this.camera.getWorldDirection(new THREE.Vector3()),
					sph = new THREE.Spherical() // 创建球坐标
				// 从Vector3中设置球坐标的radius、phi和theta值。
				sph.setFromVector3(dir)
				// MathUtils.radToDeg 角度转弧度  sph.theta 绕 y (up) 轴的赤道角(方位角)
				compass.style.transform = `rotate(${MathUtils.radToDeg(sph.theta) - 180}deg)`
			}
		}
		/**
		 * 创建效果合成器
		 */
		function createEffectComposer() {
			// 高亮显示模型（呼吸灯效果）
			const { clientHeight, clientWidth } = this.container
			effectComposer = new EffectComposer(this.renderer)
			const renderPass = new RenderPass(this.scene, this.camera)
			effectComposer.addPass(renderPass)
			outlinePass = new OutlinePass(new THREE.Vector2(clientWidth, clientHeight), this.scene, this.camera)
			outlinePass.visibleEdgeColor = new THREE.Color('#FF8C00') // 可见边缘的颜色
			outlinePass.hiddenEdgeColor = new THREE.Color('#0000') // 不可见边缘的颜色
			outlinePass.edgeGlow = 2.0 // 发光强度
			outlinePass.edgeThickness = 1 // 边缘浓度
			outlinePass.edgeStrength = 4 // 边缘的强度，值越高边框范围越大
			outlinePass.pulsePeriod = 10 // 闪烁频率，值越大频率越低
			effectComposer.addPass(outlinePass)
			const effectFXAA = new ShaderPass(FXAAShader)
			const pixelRatio = this.renderer.getPixelRatio()
			effectFXAA.uniforms.resolution.value.set(1 / (clientWidth * pixelRatio), 1 / (clientHeight * pixelRatio))
			effectFXAA.renderToScreen = true
			effectFXAA.needsSwap = true
			effectComposer.addPass(effectFXAA)
		}
		/**
		 * 切换模型
		 */
		this.onSwitchModel = function (model) {
			return new Promise(async (reslove, reject) => {
				try {
					this.clearSceneModel()
					// 重置"灯光"模块数据
					this.onResettingLight({ ambientLight: true })
					this.camera.fov = 50
					// 加载模型
					const load = await this.setModel(model)
					// 模型加载成功返回 true
					reslove({ load, filePath: model.filePath })
				} catch {
					reject()
				}
			})
		}
		/**
		 * 监听事件
		 */
		function addEvenListMouseLisatener() {
			//监听场景大小改变，跳转渲染尺寸
			window.addEventListener('resize', onWindowResize)
			this.container.addEventListener('dblclick', onCameraDistance)
		}
		/**
		 * 双击获取当前相机信息
		 */
		function onCameraDistance(e) {
			e.preventDefault()
			cameraDistance(scope.camera, scope.controls)
		}
		/**
		 * 鼠标点击事件
		 */
		this.mouseFn = function (e, callBack) {
			e.preventDefault()
			const { clientHeight, clientWidth, offsetLeft, offsetTop } = scope.container
			mouse.x = ((e.clientX - offsetLeft) / clientWidth) * 2 - 1
			mouse.y = -((e.clientY - offsetTop) / clientHeight) * 2 + 1
			// 设置起点和方向
			scope.raycaster.setFromCamera(scope.mouse, scope.camera)

			const intersects = scope.raycaster.intersectObjects(scope.scene.children)
			callBack?.(intersects)
			if (intersects.length > 0) {
				scope.mousePoint = intersects[0].point
				scope.mouseLatlog = [...scope.projection.invert([scope.mousePoint.x, scope.mousePoint.z]), scope.mousePoint.y]
			}
		}
		/**
		 * 监听窗口变化
		 * @returns
		 */
		function onWindowResize() {
			if (!scope.container) return false
			const { clientHeight, clientWidth } = scope.container
			//调整屏幕大小
			scope.camera.aspect = clientWidth / clientHeight //摄像机宽高比例
			scope.camera.updateProjectionMatrix() //相机更新矩阵，将3d内容投射到2d面上转换
			scope.renderer.setSize(clientWidth, clientHeight)
			label2dRenderer.setSize(clientWidth, clientHeight)
			label3dRenderer.setSize(clientWidth, clientHeight)
			effectComposer.setSize(clientWidth * 2, clientHeight * 2)
		}
	}
}

export {
	THREE,
	MathUtils,
	OrbitControls,
	MapControls,
	FirstPersonControls,
	EffectComposer,
	RenderPass,
	OutlinePass,
	ShaderPass,
	FXAAShader,
	CSS2DObject,
	CSS2DRenderer,
	CSS3DSprite,
	CSS3DObject,
	CSS3DRenderer,
	cameraControlsTwwen,
	cameraTwwen,
	TWEEN,
	initGrid,
	setContent,
	raycaster,
	collideCheck,
	renderModel
}
